package com.zcyr.core.impl;

import com.zcyr.core.AbstractDatabaseEnvironment;
import com.zcyr.core.AbstractDatabaseEnvironmentManager;
import com.zcyr.exception.DatabaseEnvironmentException;
import com.zcyr.exception.DatabaseEnvironmentManagerException;
import com.zcyr.mapper.DatabaseEnvironmentInfoMapper;
import com.zcyr.pojo.DatabaseEnvironmentInfo;
import com.zcyr.util.SQLTrim;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

@Component
@Slf4j
@ConditionalOnProperty(value = "tryme.databaseEnvironmentManager.enable_pool", havingValue = "true")
public class PooledDatabaseEnvironmentManager extends AbstractDatabaseEnvironmentManager {
    @Autowired
    DataSource dataSource;

    @Autowired
    private DatabaseEnvironmentPool pool;

    @Value("${spring.datasource.druid.url}")
    private String databaseUrl;

    @Value("${tryme.databaseEnvironmentManager.database}")
    private String database;

    @Autowired
    DatabaseEnvironmentInfoMapper mapper;

    private final Map<String, AbstractDatabaseEnvironment> databaseEnvironmentMap = new HashMap<>();
    private final Queue<DatabaseEnvironmentInfo> validEnvironmentInfoQue = new ConcurrentLinkedQueue<>();

    @Override
    protected void init() throws DatabaseEnvironmentException {
        super.init();
        validEnvironmentInfoQue.addAll(this.getAllInfo());
        log.info("initialized pooledDatabaseEnvironmentManager!");
    }

    private DatabaseEnvironmentInfo createEnvironmentInfo(){
        DatabaseEnvironmentInfo info = new DatabaseEnvironmentInfo();
        info.setId(generateUUID());
        info.setUsername("tryme_user_" + info.getId());
        info.setSuffix(SQLTrim.generateSuffix(info.getId()));
        info.setPassword("tryme@8848");
        info.setExpire_seconds(1800);
        return info;
    }

    @Override
    synchronized protected AbstractDatabaseEnvironment newDatabaseEnvironment(DatabaseEnvironmentInfo info) throws DatabaseEnvironmentManagerException {
        AbstractDatabaseEnvironment environment = new PooledDatabaseEnvironment(databaseUrl, info);
        try {
            if (mapper.selectById(info.getId()) != null) {
                registerEnvironment(environment);
                return environment;
            }else{
                mapper.insert(info);
            }
        } catch (Exception e){
            throw new DatabaseEnvironmentManagerException("无法创建环境", e);
        }

        try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) {
            try {
                conn.setAutoCommit(false);
                String sql = String.format(
                        "CREATE USER %s WITH CREATEROLE PASSWORD '%s'",
                        environment.getUsername(),
                        environment.getPassword()
                );
                stmt.execute(sql);
                empower(stmt, environment, null);
                registerEnvironment(environment);
                conn.commit();
                return environment;
            } catch (SQLException e) {
                log.error("无法创建数据库用户, rollback");
                conn.rollback();
                throw e;
            }
        } catch (SQLException e) {
            throw new DatabaseEnvironmentManagerException("无法创建环境!", e);
        }

    }


    /**
     * 使用有效的环境info来生成有效databaseEnvironment
     * @return 有效environment
     * @throws DatabaseEnvironmentManagerException 环境无法创建则抛出
     */
    public AbstractDatabaseEnvironment generateDatabaseEnvironment() throws DatabaseEnvironmentManagerException {
        DatabaseEnvironmentInfo info = validEnvironmentInfoQue.poll();
        if (info == null){
            info = createEnvironmentInfo();
        }
        return newDatabaseEnvironment(info);
    }


    public void registerEnvironment(AbstractDatabaseEnvironment environment){
        this.databaseEnvironmentMap.put(environment.getUuid(), environment);
        this.registerEnvironmentInfo(environment.getInfo());
    }

    @Override
    public AbstractDatabaseEnvironment getAvailableEnvironment() throws DatabaseEnvironmentManagerException {
        try{
            AbstractDatabaseEnvironment environment;
            environment = attemptErrorEnvironment(1);
            if (environment != null){
                return environment;
            }
            environment = pool.borrowObject();
            return environment;
        }catch (Exception e){
            throw new DatabaseEnvironmentManagerException(e);
        }
    }

    /**
     * 尝试启用错误环境队列中的环境
     * @param count 尝试次数, 取决于错误环境队列中有多少环境
     * @return 返回有效的环境
     */
    private AbstractDatabaseEnvironment attemptErrorEnvironment(int count){
        AbstractDatabaseEnvironment environment = null;
        for (int i=0; i<count; ++i){
            environment = this.pollErrorEnvironment();
            if (environment == null) { break; }
            try{
                environment.getConnection();
            } catch (SQLException e){
                environment.increaseErrorCounting();
                if (environment.getErrorCounting() >= 3){
                    log.error("数据库环境错误计数已达3次, 该环境将被销毁... " + environment);
                    invalidateEnvironment(environment);
                    mapper.deleteById(environment.getInfo());
                }else{
                    this.addErrorEnvironment(environment);
                }
            }
            environment.resetErrorCounting();
            try{
                releaseDatabaseEnvironment(environment.getUuid());
                return environment;
            } catch (DatabaseEnvironmentManagerException e2){
                log.error("尝试重新启用环境失败, 无法释放环境资源 env: " + environment, e2);
                invalidateEnvironment(environment);
                mapper.deleteById(environment.getInfo());
            }
        }
        return environment;
    }

    @Override
    public AbstractDatabaseEnvironment getEnvironment(String uuid) {
        return databaseEnvironmentMap.getOrDefault(uuid, null);
    }

    @Override
    public void releaseDatabaseEnvironment(String uuid) throws DatabaseEnvironmentManagerException {
        AbstractDatabaseEnvironment environment = this.databaseEnvironmentMap.getOrDefault(uuid, null);
        if (environment == null){
            throw new DatabaseEnvironmentManagerException("无法释放资源, 无效的环境uuid");
        }
        try{
            log.info(String.format("成功释放环境资源,schema: %d, table: %d, users: %d, env: %s",
                    environment.releaseSchemas(),
                    environment.releaseTables(),
                    environment.releaseUsers(),
                    environment
                    ));
        } catch (DatabaseEnvironmentException e) {
            log.error("环境资源释放失败, env: " + environment, e);
            invalidateEnvironment(environment);
            throw new DatabaseEnvironmentManagerException(e);
        }
        pool.returnObject(environment);
    }

    /**
     * 处理出现错误的异常, 尝试重连环境, 如果重连失败则从池中剔除, 并放入错误环境队列.
     * @param environment 异常环境
     * @return 返回成功重连错误环境或重新获取的新环境
     */
    @Override
    public AbstractDatabaseEnvironment reportErrorEnvironment(AbstractDatabaseEnvironment environment) throws DatabaseEnvironmentManagerException {
        for(int i=0; i<2; ++i){
            try{
                environment.getConnection();
                return environment;
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
        try{
            pool.invalidateObject(environment);
        } catch (Exception e) {
            e.printStackTrace();
        }
        this.addErrorEnvironment(environment);
        return getAvailableEnvironment();
    }

    /**
     * 将环境从manager和pool和infoMap中剔除,
     * 剔除后该环境将在本app不可用
     * @param environment 要剔除的环境
     */
    private void invalidateEnvironment(AbstractDatabaseEnvironment environment){
        this.removeEnvironmentInfo(environment.getInfo());
        databaseEnvironmentMap.remove(environment.getUuid());
        try{
            pool.invalidateObject(environment);
        } catch (Exception e) {
            log.info("无法从环境池中剔除环境: " + environment, e);
        }
    }

    private void empower(Statement stmt, AbstractDatabaseEnvironment environment, String grantRole) throws SQLException {
        String username = environment.getUsername();
        stmt.execute(String.format("GRANT CREATE ON DATABASE %s TO %s", database, username));
        if (grantRole != null) {
            stmt.execute(String.format("GRANT %s TO %s", grantRole, username));
        }
    }

    /**
     * 为环境续期
     * @param uuid 环境uuid
     */
    @Override
    public void renewal(String uuid) {
    }

    @Override
    public String status() {
        return String.format("当前环境数量: %d\n"
                + "当前环境info数量%d\n"
                + "当前环境池:"
                + "创建对象计数%d\n"
                + "借出的对象计数%d"
                + "归还的对象计数%d"
                + "销毁的对象计数%d",
                databaseEnvironmentMap.size(),
                getEnvironmentInfoMapSize(),
                pool.getCreatedCount(),
                pool.getBorrowedCount(),
                pool.getReturnedCount(),
                pool.getDestroyedCount());
    }
}
